看過了 embeddedServer().start()
,我們接著來看看 Ktor 是如何設置 Route
的
首先我們看到 embeddedServer()
裡面的參數
module = Application::module
這邊 module
內輸入的是一個函數,型態為 Application.() -> Unit
,也就是某個 Application 底下的函數,不接受參數也不會回傳。
這段 extension function 直接寫在專案內 main()
的下面,我們來看看程式碼裡面的實作:
fun Application.module() {
configureRouting()
}
這段 module 非常單純,目前只有 configureRouting
而已。
configureRouting()
不是定義在 main.kt
,而是定義在 plugins/Routing.kt
裡面
fun Application.configureRouting() {
routing {
get("/") {
call.respondText("Hello World!")
}
}
}
這邊就要提到 Ktor 的一個設計邏輯,就是包含路由在內,框架的功能功能都是以插件的形式,注入進框架使用。
這樣一來,當你不需要某個功能,你就可以自由選擇不引入該插件,減少框架本身的負擔。
那麼,這邊的 routing
是怎麼實作的呢?
@KtorDsl
public fun Application.routing(configuration: Routing.() -> Unit): Routing =
pluginOrNull(Routing)?.apply(configuration) ?: install(Routing, configuration)
如果讀者熟悉 Kotlin 的寫法的話,應該有看過這個函數的處理方式:如果一個函數內的參數只有一個,並且是接收一個匿名函數。那麼我們在使用該函數時,不需要將該參數放進小括弧內,可以直接在大括弧內撰寫該匿名函數的實作。
這邊的實作很單純:如果 pluginOrNull(Routing)
為真,那麼就只處理收到的內容。不然就必須再進行安裝 Routing
的過程。
參考一下 pluginOrNull()
的實作以及註解
/**
* Returns a plugin instance for this pipeline, or null if the plugin is not installed.
*/
public fun <A : Pipeline<*, ApplicationCall>, F : Any> A.pluginOrNull(plugin: Plugin<*, *, F>): F? {
return pluginRegistry.getOrNull(plugin.key)
}
可以得知,這段就是從 pluginRegistry
找到套件,有則回傳該套件,沒有則回傳 null。
如果有安裝過 Routing
,則直接對 Routing 加上設置即可。這邊的 apply
是 Kotlin 提供的 scope function,語言本身的使用方式我們不往下延伸說明,只需要知道這可以在 Routing 加上 configuration 即可。
如果沒有安裝的話,會進入到 install(Routing, configuration)
這段,我們來看看其註解和函數簽名:
/**
* Installs a [plugin] into this pipeline, if it is not yet installed.
*/
public fun <P : Pipeline<*, ApplicationCall>, B : Any, F : Any> P.install(
plugin: Plugin<P, B, F>,
configure: B.() -> Unit = {}
): F
根據註解說明,這段函數基本上就是安裝套件用
我們往下看看實作:
if (this is Route && plugin is BaseRouteScopedPlugin) {
return installIntoRoute(plugin, configure)
}
我們可以發現,這段會 Route 以及對應插件進行特殊處理,剛好我們目前也只需要理解這段邏輯,可以直接先看 installIntoRoute
。
installIntoRoute
的實作如下:
private fun <B : Any, F : Any> Route.installIntoRoute(
plugin: BaseRouteScopedPlugin<B, F>,
configure: B.() -> Unit = {}
): F {
if (pluginRegistry.getOrNull(plugin.key) != null) {
throw DuplicatePluginException(
"Please make sure that you use unique name for the plugin and don't install it twice. " +
"Plugin `${plugin.key.name}` is already installed to the pipeline $this"
)
}
if (application.pluginRegistry.getOrNull(plugin.key) != null) {
throw DuplicatePluginException(
"Installing RouteScopedPlugin to application and route is not supported. " +
"Consider moving application level install to routing root."
)
}
前面兩段是錯誤處理,
// we install plugin into fake pipeline and add interceptors manually
// to avoid having multiple interceptors after pipelines are merged
val fakePipeline = when (this) {
is Routing -> Routing(application)
else -> Route(parent, selector, developmentMode, environment)
}
val installed = plugin.install(fakePipeline, configure)
pluginRegistry.put(plugin.key, installed)
mergePhases(fakePipeline)
receivePipeline.mergePhases(fakePipeline.receivePipeline)
sendPipeline.mergePhases(fakePipeline.sendPipeline)
addAllInterceptors(fakePipeline, plugin, installed)
receivePipeline.addAllInterceptors(fakePipeline.receivePipeline, plugin, installed)
sendPipeline.addAllInterceptors(fakePipeline.sendPipeline, plugin, installed)
return installed
}
這邊解釋了函數設計的邏輯:我們先定義一個假的 Pipeline,然後將攔截器(interceptor)一個個加入,避免加入多個攔截器。
這邊我們也可以看到 pluginRegistry.put(plugin.key, installed)
,所以安裝過之後,前面的 pluginRegistry.getOrNull(plugin.key)
就可以知道這個套件已經安裝過了。
今天我們追到 routing()
的設計,看到 Ktor 如何透過一系列的流程,安裝上程式內撰寫的路由。
沒想到光是要加入一個路由,邏輯就已經很多了!之後的部分我們就明天再看吧!